Skip to content

feat(libexec): bklibcvenv — extract glibc wrapper-script pattern from pantry@5354c73f#348

Open
tannevaled wants to merge 2 commits into
pkgxdev:mainfrom
tannevaled:feat/bklibcvenv
Open

feat(libexec): bklibcvenv — extract glibc wrapper-script pattern from pantry@5354c73f#348
tannevaled wants to merge 2 commits into
pkgxdev:mainfrom
tannevaled:feat/bklibcvenv

Conversation

@tannevaled
Copy link
Copy Markdown

Closes #344.

What

Extracts the wrapper-script pattern @jhheider just validated inline in pkgxdev/pantry@5354c73f (the post-merge follow-up on the glibc PR) into a reusable brewkit helper.

Mirrors the shape of libexec/bkpyvenv:

  • libexec/bklibcvenv — the recipe-side helper
  • share/brewkit/libcvenv-wrapper.sh — the POSIX sh wrapper template

Recipe-side API

build:
  script:
    # …make install, etc.
    - bklibcvenv seal {{prefix}} glibc-{{version.marketing}}

vs. the current inline shape (~20 lines of for-loop + sed + chmod + an embedded prop: template).

Behaviour

For each ELF in <prefix>/{bin,sbin}/* with a PT_INTERP:

  1. Move it to <prefix>/libexec/<libc>-<dir>/ (where <libc> = "glibc" for glibc-2.43, "musl" for musl-1.2.x, etc.)
  2. Replace <prefix>/<dir>/<name> with a generated POSIX sh wrapper

Generated wrapper computes its own bindir + climbs to prefix at runtime, then exec's ld.so --library-path … <libexec-path> "$@". Fully relocatable — works at /opt, ~/.pkgx, anywhere.

LDSO is auto-detected from uname -m (overridable via LDSO env var for exotic loaders).

Why this matters

  • @jhheider implemented the pattern inline in glibc and it worked. Three more libc recipes (musl, …) or older glibc versions would duplicate the same 20-line block. Cheap to extract.
  • The Linux pattern empirically validates the Windows analog sketched at feat(windows): sketch fix-pe.ts + bkwinvenv (skeletons for #346) #347bkwinvenv would have the exact same shape, just .cmd wrappers + Windows DLL search-order.
  • Independently useful for cross-distro deployment: anyone shipping a glibc-linked binary that needs to run on Alpine/musl can bklibcvenv seal to bundle.

Migration plan

Once this lands, the inline block in pkgxdev/pantry's glibc recipe (currently in commit @5354c73f) becomes a one-liner. Same for any future libc bottle.

Open questions

  1. bklibcvenv stage?bkpyvenv has both stage and seal. For libc, there's no equivalent of "create a venv" at build time — the recipe just builds libc normally; the only post-step is seal. So I left out stage. Worth adding for symmetry?
  2. Auto-detect <libdir-name>? — recipe passes it explicitly today; the helper could probably auto-detect from the only glibc-X.Y/ dir under lib/. Brittle if multiple bottles ever coexist there.
  3. POSIX sh portability — wrapper uses CDPATH= cd -- "$bindir/.."; the -- is bash-friendly but POSIX-ambiguous on some old shells. Open to a tighter form.

Testing

Not yet validated end-to-end (no CI run on this PR). The intent is to replace the inline block in pkgxdev/pantry's glibc recipe with bklibcvenv seal … and confirm CI still passes — that's the natural test.

Refs: #344 (RFC this closes), #346 (Windows port RFC — same pattern), #347 (bkwinvenv sketch — Windows analog).

Extracts the recipe-side hermetic libc-bundling pattern @jhheider
just validated inline in pkgxdev/pantry@5354c73f. Recipes that ship
their own libc (glibc today; musl, custom libcs tomorrow) get a
~one-line call:

  - bklibcvenv seal {{prefix}} glibc-{{version.marketing}}

instead of the ~20-line inline wrapper-installer that's currently
duplicated in the glibc recipe.

## Shape

Mirror of libexec/bkpyvenv (Python venv → relocatable stubs):

  libexec/bklibcvenv                 — the helper script
  share/brewkit/libcvenv-wrapper.sh  — the POSIX sh wrapper template

The helper:
  1. takes `<prefix> <libdir-name>` as args
  2. auto-detects LDSO from `uname -m` (overridable via LDSO env)
  3. for each ELF in <prefix>/{bin,sbin}/* with a PT_INTERP:
       - moves it to <prefix>/libexec/<libc-name>-<dir>/
       - replaces it with a sed-templated sh wrapper
  4. wrapper does `cd $(dirname $0)/..` to compute prefix at
     runtime, then `exec $libdir/$ldso --library-path $libdir
     $libexec_path "$@"`

`<libc-name>` (libdir-name up to first '-') prefixes the libexec
subdir so multiple libc bottles can coexist (glibc + musl):
  - libexec/glibc-bin/
  - libexec/glibc-sbin/

## Why this is useful

- **Fully relocatable** — bottle extracted at `/opt`, `~/.pkgx`, or
  any other path works the same; no absolute paths baked in.
- **Sidesteps PT_INTERP** — invoking ld.so explicitly via wrapper
  ignores the ELF's PT_INTERP. brewkit's fix-elf currently can't
  rewrite PT_INTERP anyway (pkgxdev#345) and the bottle's
  own ld.so suffers from fix-elf's RPATH pollution when not
  `build.skip: fix-patchelf`'d.
- **Reusable** — same shape works for musl, dietlibc, or any
  alternate libc anyone packages.

## Connection to bkwinvenv (pkgxdev#347)

The Windows analog (`bkwinvenv`, sketched at pkgxdev#347) uses the same
seal-into-libexec + emit-wrapper-script structure, just with `.cmd`
wrappers + Windows DLL search-order semantics (no explicit loader
invocation; co-location with the inner .exe is enough). Validating
this Linux pattern empirically (which @jhheider's commit does)
validates the Windows design too.

## Migration

Once this lands, pkgxdev/pantry's glibc recipe can shrink its
inline wrapper-installer block (currently ~20 lines + a prop:
template) to one line. Same for any future libc recipe.

Refs: pkgxdev#344 (this RFC), pkgxdev#347
(bkwinvenv sibling), pkgxdev/pantry@5354c73f (origin of pattern).
@tannevaled
Copy link
Copy Markdown
Author

Proposed glibc-recipe migration delta (for review purposes — not a PR yet).

After this PR lands, pkgxdev/pantry/projects/gnu.org/glibc/package.yml collapses the wrapper-installer block from ~30 lines to ~1:

-    # PT_INTERP is absolute and cannot use $ORIGIN or fall back. Do not
-    # bake {{prefix}} or its post-rename variant into installed binaries:
-    # that breaks when the bottle relocates to ~/.pkgx. Instead, seal the
-    # glibc-owned ELF tools into libexec and replace bin/* / sbin/* with
-    # POSIX sh wrappers that invoke this bottle's ld.so by relative path.
-    #
-    # Running an ELF through ld.so explicitly ignores the ELF's own
-    # PT_INTERP, so the inner libexec binaries may keep whatever glibc's
-    # build installed. The wrapper supplies --library-path explicitly.
-    - run: |
-        for dir in bin sbin; do
-          mkdir -p "{{prefix}}/libexec/glibc-$dir"
-          for f in "{{prefix}}/$dir"/*; do
-            [ -f "$f" ] && [ ! -L "$f" ] || continue
-            patchelf --print-interpreter "$f" >/dev/null 2>&1 || continue
-            mv "$f" "{{prefix}}/libexec/glibc-$dir"
-            sed -e "s/@LDSO@/$LDSO/g" -e "s/@DIR@/$dir/g" "$PROP" >$f
-            chmod 775 $f
-            echo "wrapped $f"
-          done
-        done
-      prop: |
-        #!/bin/sh
-
-        case "$0" in
-          */*) bindir=${0%/*} ;;
-          *) bindir=$(command -v -- "$0"); bindir=${bindir%/*} ;;
-        esac
-
-        prefix=$(CDPATH= cd -- "$bindir/.." && pwd)
-
-        libdir="$prefix/lib/glibc-{{version.marketing}}"
-
-        exec "$libdir/@LDSO@" --library-path "$libdir" "$prefix/libexec/glibc-@DIR@/$(basename "$0")" "$@"
+    # Seal bin/* + sbin/* into libexec/glibc-{bin,sbin}/ and replace
+    # them with relocatable POSIX sh wrappers that route through this
+    # bottle's ld.so. See pkgxdev/brewkit#348.
+    - bklibcvenv seal {{prefix}} glibc-{{version.marketing}}

The other recipe edits in @jhheider's 5354c73f stay as-is (the linker-script-basename sed; the LDSO env var which is still used by the bin/ld.so symlink step + test block).

Net delta on glibc/package.yml: ~28 lines removed, 1 line added.

If you'd rather I open the migration as a stacked pantry PR, happy to — but its CI would stay red until this brewkit PR lands, so it'd be noise. Easier to merge in lockstep: this PR + a 1-commit pantry PR side-by-side.

@tannevaled tannevaled marked this pull request as ready for review May 22, 2026 21:46
Copilot AI review requested due to automatic review settings May 22, 2026 21:46
@tannevaled
Copy link
Copy Markdown
Author

Extra evidence the inline shape works (@jhheider's CD run on main after 5354c73f): https://github.com/pkgxdev/pantry/actions/runs/26311544237

✓ pkg / gnu.org/glibc / *nix64 / test *nix64 archlinux:base   (51s)
✓ pkg / gnu.org/glibc / *nix64 / test *nix64 ubuntu            (46s)
✓ pkg / gnu.org/glibc / *nix64 / test *nix64 debian:buster-slim (45s)
✓ pkg / gnu.org/glibc / *nix·ARM64 / test                       (1m27s)
✓ bottle (*nix64.xz) ✓ bottle (*nix·ARM64.xz)

So the relocatable wrapper-script approach works in production across 3 Linux distros + ARM64 + the buster-slim test sandbox (which was where everything was breaking earlier). That's the empirical case for the pattern; extracting it here as a brewkit-provided helper is just removing duplication.

This PR is ready for review whenever you have a moment.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds a bklibcvenv helper and wrapper template to make bundled libc bottles relocatable by routing packaged ELF binaries through the bottle’s ld.so via relative paths.

Changes:

  • Introduce bklibcvenv seal to wrap ELF binaries in bin/ and sbin/ using a wrapper template.
  • Add a POSIX-sh wrapper template that resolves prefix at runtime and execs the bundled loader with --library-path.
  • Auto-detect loader name (LDSO) based on uname -m, with env override.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.

File Description
share/brewkit/libcvenv-wrapper.sh Adds the runtime wrapper template used to exec binaries through the bundled ld.so.
libexec/bklibcvenv Adds the sealing tool that moves ELF binaries to libexec/ and generates wrappers from the template.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread libexec/bklibcvenv Outdated
Comment on lines +38 to +39
CMD=$1; shift

Comment thread libexec/bklibcvenv Outdated
exit 2
fi

set -x
Comment thread libexec/bklibcvenv Outdated
-e "s|@LIBC_NAME@|$LIBC_NAME|g" \
-e "s|@DIR@|$dir|g" \
"$TEMPLATE" > "$f"
chmod 775 "$f"
Comment thread share/brewkit/libcvenv-wrapper.sh Outdated
Comment on lines +12 to +15
*) bindir=$(command -v -- "$0"); bindir=${bindir%/*} ;;
esac

prefix=$(CDPATH= cd -- "$bindir/.." && pwd)
Comment thread libexec/bklibcvenv
Comment on lines +88 to +93
sed \
-e "s|@LDSO@|$LDSO|g" \
-e "s|@LIBDIR@|$LIBDIR_NAME|g" \
-e "s|@LIBC_NAME@|$LIBC_NAME|g" \
-e "s|@DIR@|$dir|g" \
"$TEMPLATE" > "$f"
All 5 points were legit:

1. **shift on empty args** (libexec/bklibcvenv:39) — with set -eo
   pipefail, `CMD=$1; shift` exited before the usage-error branch
   could fire when called with zero args. Now: check `$#` first.

2. **set -x unconditional** (line 73) — floods CI output. Gated
   behind BKLIBCVENV_DEBUG env var.

3. **chmod 775** (line 94) — group-writable on packaged artefacts
   is a bad-practice flag for security/packaging audits. Changed
   to 755.

4. **POSIX `--` in command -v / cd** (template line 12, 15) — not
   specified by POSIX for these built-ins; some old /bin/sh
   implementations reject. Dropped `--`; in our context $0 is
   never user-controlled so the defense was moot.

5. **sed-replacement metacharacter unsafety** (line 93) — values
   containing `&`, `\`, or the `|` delimiter would corrupt sed
   output. Defense in depth: added a `sed_escape()` helper and
   applied to all four interpolated values before substitution.

Values in question (LDSO, LIBDIR_NAME, LIBC_NAME, dir) are all
auto-detected or recipe-passed and won't realistically contain
sed metachars, but Copilot's right that this should not be a
latent footgun.
@tannevaled
Copy link
Copy Markdown
Author

Addressed all 5 Copilot review comments in 9ed9496:

Comment Fix
shift exits before usage error when args missing Check $# before shift
set -x is unconditional spam Gate behind BKLIBCVENV_DEBUG env
chmod 775 on packaged artefacts Changed to 755
-- end-of-options not POSIX for cd/command -v Dropped -- (our $0 is never user-controlled)
sed replacement metachars (&, \, ` `) unescaped

Values in question are tightly controlled (LDSO from uname, LIBDIR_NAME/LIBC_NAME from recipe args) but defense in depth is the right call.

Copy link
Copy Markdown
Contributor

@jhheider jhheider left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks good

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bklibcvenv: hermetic glibc helper (analogue to bkpyvenv)

3 participants